package game;

import game.entities.IEnemyEntity;
import game.entities.players.Player;
import game.graphics.Block;

import java.awt.Rectangle;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;

import application.Configuration;
import application.messages.commands.parameters.Coordinates2D;
import data.DataManager;
import data.DataManager.BlockType;

/**
 * This class implements collision checker.
 * It detects collisions for whole list of
 * collidable objects and send messages to them.
 * 
 * @author Drahomir Karchnak, Tomas Zima
 */
public class CollisionChecker implements ICollisionCheck, Serializable
{
	/**
	 * Constructor.
	 * Save reference to blocks (check collision
	 * of objects with map).
	 * 
	 * @param blocks
	 */
	CollisionChecker(Block blocks[][])
	{
		this.blocks = blocks;
	}
	
	/**
	 * Add object to collision checker. Without this,
	 * collisions wouldn't be calculated for this object.
	 * 
	 * @param object
	 */
	public void addCollidableObject(ICollidable object)
	{
		this.collidableObjects.add(object);
	}
	
	/**
	 * Check collisions for everything and send messages 
	 * to objects.
	 */
	public void checkCollisions()
	{
		boolean repeat = false;
		
		for (int i = 0; i < collidableObjects.size(); i++)
		{
			if (collidableObjects.get(i).isCollidableWithMap())
			{
				if (rectangleInCollision(collidableObjects.get(i).getBoundingBoxes()[0]))
				{
					if (collidableObjects.get(i).isSensitivelyDestroyable())
					{
						collidableObjects.get(i).destroy();
						collidableObjects.remove(i);
						repeat = true;
						continue;
					}
					else
					{
						collidableObjects.get(i).removeCollision();
					}
					
					/*
					 * This calculates point in the middle of down side of bounding box.
					 * It's useful to check if Player is in collision with ground and
					 * so he's able to jump or not.
					 */
					// Rectangle (pretty dirty, change it to point sometimes)
					Rectangle groundCollisionPoint = new Rectangle();
					
					// Calculate the position in the Y axis
					groundCollisionPoint.y = 
							collidableObjects.get(i).getBoundingBoxes()[0].y + 
							collidableObjects.get(i).getBoundingBoxes()[0].height + 4;
					
					// Calculate the position in the X axis
					groundCollisionPoint.x = 
							(collidableObjects.get(i).getBoundingBoxes()[0].x + 
							 collidableObjects.get(i).getBoundingBoxes()[0].x + 
							 collidableObjects.get(i).getBoundingBoxes()[0].width) / 2;
					
					// It's size is equal to one pixel
					groundCollisionPoint.width = 1;
					groundCollisionPoint.height = 1;
					
					// Check collisions
					if (rectangleInCollision(groundCollisionPoint))
					{
						collidableObjects.get(i).collidesDown();
					}
					else
					{
						collidableObjects.get(i).notCollidesDown();
					}
				}
				else
				{
					collidableObjects.get(i).notInCollision();
					collidableObjects.get(i).notCollidesDown();
				}
			}
		}
		
		if (repeat)
		{
			repeat = false;
			checkCollisions();
		}

		/*
		 * Iterate through all collidable objects and find gunshots
		 * and enemies or other things, which can be destroyed by
		 * gunshots.
		 */
		
		// Every collidable object must be compared with each other
		// collidable object
		for (int i = 0; i < collidableObjects.size(); i++)
		{
			for (int j = 0; j < collidableObjects.size(); j++)
			{
				// Get both bounding boxes
				Rectangle r1 = collidableObjects.get(i).getBoundingBoxes()[0];
				Rectangle r2 = collidableObjects.get(j).getBoundingBoxes()[0];
				
				// If one object is a gunshot
				if (collidableObjects.get(i).hasDestroyingCharacter())
				{
					// If second object is damagable by gunshot
					if (collidableObjects.get(j).isDamagable())
					{
						// If these objects collides
						if (r1.intersects(r2))
						{
							// Damage object
							collidableObjects.get(i).destroy();
							collidableObjects.get(j).damage(-1);
						}
					}
				}
			}
		}
		
		for (int i = 0; i < collidableObjects.size(); i++)
		{
			if (collidableObjects.get(i).isPlayerControlled())
			{
				for (int j = 0; j < collidableObjects.size(); j++)
				{
					Rectangle r1 = collidableObjects.get(i).getBoundingBoxes()[0];
					Rectangle r2 = collidableObjects.get(j).getBoundingBoxes()[0];
					
					if (collidableObjects.get(j).isEnemy())
					{
						if (r1.intersects(r2))
						{
							collidableObjects.get(i).damage(20);

							Player p = (Player) collidableObjects.get(i);
							p.setImunity(100);
						}
					}
				}
			}
		}
	}

	/**
	 * Checks if rectangle is in collision with map
	 * or not.
	 * 
	 * @param r
	 * @return
	 */
	public boolean rectangleInCollision(Rectangle r)
	{
		int TileX = r.x / 32; 
		int TileY = r.y / 32;
		Rectangle TileR = new Rectangle();
		
		for(int i = TileX - 3; i < TileX + 3; i++)
		{
			for(int j = TileY - 3; j < TileY + 3; j++)
			{	
				try
				{
					//if(blocks[i][j].getBlockType() != DataManager.Type.BLOCK_NONE)
					if (isBlockCollidable(blocks[i][j].getBlockType()))
					{	
						TileR.x = i * Configuration.BLOCK_SIZE;
						TileR.y = j * Configuration.BLOCK_SIZE;
						TileR.width = TileR.height = Configuration.BLOCK_SIZE;
						
						if (TileR.intersects(r)) return true;
					}
				}
				catch (ArrayIndexOutOfBoundsException errHandler)
				{
					//
				}
			}
		}
		
		return false;
	}
	
	/**
	 * Checks if block is collidable (player can go through it or not).
	 * 
	 * @param blockType
	 * @return
	 */
	protected boolean isBlockCollidable(DataManager.BlockType blockType)
	{
		/*
		 * For some objects, collisions are not calculated. The reason is
		 * very simple. For example, if you are just going in the front of
		 * tree, it wouldn't be very comfortable if every tree is collidable
		 * object.
		 * 
		 * Beside that, it's better to have these objects as none-collidable
		 * and let them in "background".
		 */
		switch (blockType)
		{
			case BLOCK_CACTUS:
			case BLOCK_CACTUS_BREAK:
			case BLOCK_CACTUS_TOP:
			case BLOCK_CACTUS_TOP_BREAK:

			case BLOCK_FLOWER_RED:
			case BLOCK_FLOWER_YELLOW:
			case BLOCK_NONE:

			case BLOCK_SAMPLING:

			case BLOCK_SHROOM_BROWN:
			case BLOCK_SHROOM_RED:

			case BLOCK_TREETOP_BOTTOM_LEFT:
			case BLOCK_TREETOP_BOTTOM_MIDDLE:
			case BLOCK_TREETOP_BOTTOM_RIGHT:
			case BLOCK_TREETOP_MIDDLE_LEFT:
			case BLOCK_TREETOP_MIDDLE_MIDDLE:
			case BLOCK_TREETOP_MIDDLE_RIGHT:
			case BLOCK_TREETOP_TOP_LEFT:
			case BLOCK_TREETOP_TOP_MIDDLE:
			case BLOCK_TREETOP_TOP_RIGHT:
			case BLOCK_TREE_BOTTOM:
			case BLOCK_TREE_BOTTOM_BREAK:
			case BLOCK_TREE_LEAVES_LEFT:
			case BLOCK_TREE_LEAVES_RIGHT:
			case BLOCK_TREE_MIDDLE:
			case BLOCK_TREE_MIDDLE_BREAK:
			case BLOCK_TREE_MIDDLE_LEFT:
			case BLOCK_TREE_MIDDLE_LEFT_BREAK:
			case BLOCK_TREE_MIDDLE_RIGHT:
			case BLOCK_TREE_MIDDLE_RIGHT_BREAK:
				return false;
		}
		
		return true;
	}
	
	/**
	 * Finds defined gunshot in the list of all collidable objects and 
	 * destroys it.
	 * @param gunshot
	 */
	public void removeGunshot(Gunshot gunshot)
	{
		for (int i = 0; i < collidableObjects.size(); i++)
		{
			if (collidableObjects.get(i).equals(gunshot))
			{
				collidableObjects.remove(i);
				break;
			}
		}
	}
	
	/**
	 * Finds defined general enemy in the list of all collidable objects
	 * and destroys it.
	 * 
	 * @param enemy
	 */
	public void removeGeneralEnemy(IEnemyEntity enemy)
	{
		for (int i = 0; i < collidableObjects.size(); i++)
		{
			if (collidableObjects.get(i).equals(enemy))
			{
				collidableObjects.remove(i);
				break;
			}
		}
	}
	
	/**
	 * Finds a free place (quad) with defined size.
	 *  
	 * @param bestX
	 * 	The best suitining coordinate in the X axis.
	 * 
	 * @param bestY
	 * 	The best suiting coordinate in the Y axis.
	 * 
	 * @param size
	 * 	Size of quad.
	 * 
	 * @param maxDistance
	 * 	Maximal distance from ideal state.
	 * 
	 * @return
	 */
	public Coordinates2D getFreeSpace(int bestX, int bestY, int size, int maxDistance)
	{
		// Calculate minimal X and Y
		int minimalX = bestX - maxDistance;
		int maximalX = bestX + maxDistance;
		
		// Calculate maximal X and Y
		int minimalY = bestY - maxDistance;
		int maximalY = bestY + maxDistance;
		
		// Set start position for searching
		int x = minimalX;
		int y = minimalY;
		
		// Default state: not found
		boolean found = false;
		
		// Finding algorithm
		for (; x < maximalX; x++)
		{
			for (; y < maximalY; y++)
			{
				if (isPlaceFree(x, y, size))
				{
					found = true;
					break;
				}
			}
		}
		
		/*
		 * Return result.
		 */
		if (found)
		{
			return new Coordinates2D(x, y);
		}
		else
		{
			return null;
		}
	}
	
	private boolean isPlaceFree(int positionX, int positionY, int size)
	{
		boolean free = true;

		try
		{
			for (int x = positionX; x < positionX + size; x++)
			{
				for (int y = positionY; y < positionY + size; y++)
				{
					if (blocks[x][y].getBlockType() != BlockType.BLOCK_NONE)
					{
						free = false;
						break;
					}
				}
			}
		}
		catch (ArrayIndexOutOfBoundsException errHandler)
		{
			free = false;
		}
		
		return free;
	}
	
	private List<ICollidable> collidableObjects = new LinkedList<ICollidable>();
	private Block blocks[][];
}
